/*
 * Analyzer.java
 */
package behavior_analysis_model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import behavior_analysis_control.DialogErrorException;

/**
 * Analyzer contains methods to analyze Track objects and 
 * find all points of interest, condensing these to allow for further
 * analysis.
 * 
 * @author Sean Brown
 * @version 11/14/2012
 */
public class Analyzer {
	
	/**
	 * Number of milliseconds in a second.
	 */
	public static final long MIL_TO_SEC = 1000;
	
	/**
	 * Number of seconds in a minute.
	 */
	public static final long SEC_TO_MIN = 60;
	
	/**
	 * Minimum time (in minutes) an object must remain in an area to be 
	 * considered a point of interest.
	 */
	private static final long MIN_TIME = 5;
	
	/**
	 * Maximum distance (in feet) an object can move in any direction before it
	 * is determined out of range.
	 */
	private static final double MAX_MOVE = 300.0;
	
	/**
	 * Factor for converting feet to GPS coordinate distance.
	 */
	private static final double FACTOR = 0.00000058;
	
	/**
	 * Map of Tracks as keys to values that contain all of the TimedTracks for that
	 * Track, which is a point of interest.
	 */
	private final Map<Track, ArrayList<TimedTrack>> my_pois;
	
	/**
	 * List of all TimedTracks that have been found during analysis.
	 */
	private final ArrayList<TimedTrack> my_all_locs;
	
	/**
	 * Constructor instantiates objects.
	 */
	public Analyzer() {
		my_pois = new HashMap<Track, ArrayList<TimedTrack>>();
		my_all_locs = new ArrayList<TimedTrack>();
	}

	/**
	 * Method analyzes a list of Track objects (which must be in sequential order)
	 * and finds and saves all POI's for those Tracks.
	 * 
	 * @param the_tracks The Tracks containing date/time/gps info.
	 */
	public void analyzeTracks(final List<Track> the_tracks, final Progress progress) {
		//Create real clone of the List, so you don't clobber the originals
		final ArrayList<Track> tracks = new ArrayList<Track>();
		for (Track t : the_tracks) {
			tracks.add(t.clone());
		}
		//sort the tracks by time ascending
		//this does not work if the time has no "seconds" fields,
		//so the file must be in order of time originally.
		Collections.sort(tracks, new TrackComparator());
		
		//Find locations of interest
		my_all_locs.clear();
		FastMedian<Double> lats = new FastMedian<Double>();
		FastMedian<Double> lgts = new FastMedian<Double>();
		long start_time = 0;
		boolean new_location = true;
		for (int i = 0; i < tracks.size(); i++) {
			if (progress != null) {
				progress.setProgress(i * 100 / tracks.size());
			}
			
			//get the current track
			final Track current = tracks.get(i);
			//new location, no comparison needed.
			if (new_location) {
				//System.out.println("New location.");
				lats.clear();
				lats.add(current.getLatitude());
				lgts.clear();
				lgts.add(current.getLongitude());
				start_time = current.getDateTime();
				new_location = false;
			} else { //new_location already recorded.
				final long total_time = current.getDateTime() - start_time;
				//System.out.println("Total time: " + total_time + " milliseconds.");
				//determine if the thing being tracked has moved out of the current acceptable radius
				boolean isInRange = isInRangeFast(current.getLatitude(), current.getLongitude(), 
								medianFast(lats), medianFast(lgts), MAX_MOVE);
				//If the track is in the range of the previous tracks.
				if (isInRange) {
					//add the coordinates weighted by time (add one entry per minute)
					for (int j = 0; j < (total_time / (MIL_TO_SEC * SEC_TO_MIN)); j++) {
						lats.add(current.getLatitude());
						lgts.add(current.getLongitude());
						//System.out.println(current.getLatitude() + ", " + current.getLongitude() + " added to FastMedian.");
					}
				} 
				//If this is the last track, treat is as if it has moved out of range.
				if (!isInRange || i == tracks.size() - 1) {
					//if the amount of time is more than the designated maximum time (in minutes)
					if (total_time > MIN_TIME * MIL_TO_SEC * SEC_TO_MIN) {
						//add the location and total time to the list
						my_all_locs.add(new TimedTrack(medianFast(lats), medianFast(lgts), 
								start_time, current.getDateTime()));
						//System.out.println(my_all_locs.get(my_all_locs.size() - 1) + " added.");
					}
					new_location = true;
				}
			}
		}
		findPOIs(MAX_MOVE);
	}
	
	/**
	 * Returns the map of POIs and all related TimedTracks.
	 * 
	 * @return Map<Track, ArrayList<TimedTrack>> of POIs and their individual TimedTracks.
	 * @throws DialogErrorException 
	 */
	public Map<Track, ArrayList<TimedTrack>> getPOIs() throws DialogErrorException {
		if (my_all_locs.isEmpty()) {
			throw new DialogErrorException("You must first run analyzeTracks method on a valid list of Track objects.");
		}
		if (my_pois.isEmpty()) {
			throw new DialogErrorException("No points of interest found.");
		}
		return my_pois;
	}
	
	/**
	 * From the list of TimedTracks, finds the points of interest.
	 * 
	 * @param the_range How large the radius is (in feet) to group clusters of GPS coords.
	 */
	private void findPOIs(final double the_range) {
		final ArrayList<TimedTrack> copy = new ArrayList<TimedTrack>();
		for (TimedTrack tt : my_all_locs) {
			copy.add(tt);
		}
		while (copy.size() > 0) {
			final TimedTrack curr = copy.remove(0);
			
			FastMedian<Double> lats = new FastMedian<Double>();
			FastMedian<Double> lgts = new FastMedian<Double>();
			lats.add(curr.getLatitude());
			lgts.add(curr.getLongitude());
			long total_time = curr.getTotalTime();
			final Iterator<TimedTrack> itr = copy.iterator();
			ArrayList<TimedTrack> tt = new ArrayList<TimedTrack>();
			tt.add(curr);
			while (itr.hasNext()) {
				final TimedTrack next = itr.next();
				if (isInRangeAccurate(medianFast(lats), medianFast(lgts),
						next.getLatitude(), next.getLongitude(), the_range)) {
					lats.add(next.getLatitude());
					lgts.add(next.getLongitude());
					total_time += next.getTotalTime();
					tt.add(next);
					itr.remove();
				}
			}
			Track key = new Track(medianFast(lats), medianFast(lgts), total_time);
			my_pois.put(key, tt);
		}
	}
	
	/**
	 * Uses advanced calculation to determine the distance between two GPS coordinates and
	 * returns whether or not this distance is within the range value passed.
	 * 
	 * @param lat1 Latitude 1
	 * @param lon1 Longitude 1
	 * @param lat2 Latitude 2
	 * @param lon2 Longitude 2
	 * @param range How large the radius is (in feet) to determine if these coordinates are in range of each other.
	 * @return boolean are in range or not in range.
	 */
	public static boolean isInRangeAccurate(final double lat1, final double lon1, final double lat2, final double lon2, final double range) {
		//Taken from: http://www.ehow.com/how_7695232_convert-gps-coordinates-feet.html
		//From GPS coordinates (a1, b1) and (a2, b2):
		//131332796.6 * ArcCos{Cos[a1]xCos[b1]xCos[a2]xCos[b2] + Cos[a1]xSin[b1]xCos[a2]xSin[b2] + Sin[a1]xSin[a2]}/360);
		return (131332796.6 * (Math.acos((Math.cos(lat1) * Math.cos(lon1) * Math.cos(lat2) * Math.cos(lon2)) + 
				(Math.cos(lat1) * Math.sin(lon1) * Math.cos(lat2) * Math.sin(lon2)) + 
				(Math.sin(lat1) * Math.sin(lat2))) / 360)) < range;
	}
	
	/**
	 * Uses basic calculation to determine the distance between two GPS coordinates and 
	 * returns whether or not this distance is within the range value passed.
	 * 
	 * @param lat1 Latitude 1
	 * @param lon1 Longitude 1
	 * @param lat2 Latitude 2
	 * @param lon2 Longitude 2
	 * @param range How large the radius is (in feet) to determine if these coordinates are in range of each other.
	 * @return boolean are in range or not in range. 
	 */
	public static boolean isInRangeFast(final double lat1, final double lon1, final double lat2, final double lon2, final double range) {
		if (Math.abs(Math.abs(lat1) - Math.abs(lat2)) + (Math.abs(lon1) - Math.abs(lon2)) < (range * FACTOR)) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * Takes a list of double values and calculates the average of all values.
	 * 
	 * @param the_doubles The list of double values.
	 * @return The average of the double values.
	 */
	public static double average(final ArrayList<Double> the_doubles) {
		double sum = 0;
		if (the_doubles.size() > 1) {
			for (final double d : the_doubles) {
				sum += d;
			}
			sum = trimCoord(sum / the_doubles.size());
		} else if (the_doubles.size() == 1) {
			sum = the_doubles.get(0);
		} else {
			sum = -1;
		}
		return sum;
	}
	
	/**
	 * Takes a FastMedian tree and grabs the root (median) value.
	 * 
	 * @param the_doubles The FastMedian tree.
	 * @return The median value of the tree.
	 */
	private static double medianFast(final FastMedian<Double> the_doubles) {
		return the_doubles.getMedian();
	}
	
	/**
	 * Determines the median of a List of double values.
	 * NOT CURRENTLY IN USE
	 * 
	 * @param the_doubles The list of double values.
	 * @return The median of the list.
	 */
	private static double medianList(final ArrayList<Double> the_doubles) {
		double median = -1.0;
		if (the_doubles.size() == 1) {
			median = the_doubles.get(0);
		} else if(the_doubles.size() > 1) {
			Collections.sort(the_doubles);
			median = the_doubles.get(the_doubles.size() / 2);
		}
		return median;
	}
	
	/**
	 * Trims a coordinate value to 7 decimal points.
	 * 
	 * @param the_coord The coordinate value
	 * @return The coordinate value trimmed to 7 decimal points.
	 */
	public static double trimCoord(final double the_coord) {
		return (double)Math.round(the_coord * 10000000) / 10000000;
	}

}
